// Package item provides the default functionality to Ponzu's content/data types,
// how they interact with the API, and how to override or enhance their abilities
// using various interfaces.
package item

import (
	"fmt"
	"net/http"
	"regexp"
	"strings"
	"unicode"

	"github.com/blevesearch/bleve"
	"github.com/blevesearch/bleve/mapping"
	"github.com/gofrs/uuid"
	"golang.org/x/text/transform"
	"golang.org/x/text/unicode/norm"
)

var rxList map[*regexp.Regexp][]byte

func init() {
	// Compile regex once to use in stringToSlug().
	// We store the compiled regex as the key
	// and assign the replacement as the map's value.
	rxList = map[*regexp.Regexp][]byte{
		regexp.MustCompile("`[-]+`"):                                                                         []byte("-"),
		regexp.MustCompile("[[:space:]]"):                                                                    []byte("-"),
		regexp.MustCompile("[[:blank:]]"):                                                                    []byte(""),
		regexp.MustCompile("`[^a-z0-9]`i"):                                                                   []byte("-"),
		regexp.MustCompile("[!/:-@[-`{-~]"):                                                                  []byte(""),
		regexp.MustCompile("/[^\x20-\x7F]/"):                                                                 []byte(""),
		regexp.MustCompile("`&(amp;)?#?[a-z0-9]+;`i"):                                                        []byte("-"),
		regexp.MustCompile("`&([a-z])(acute|uml|circ|grave|ring|cedil|slash|tilde|caron|lig|quot|rsquo);`i"): []byte("\\1"),
	}
}

// Sluggable makes a struct locatable by URL with it's own path.
// As an Item implementing Sluggable, slugs may overlap. If this is an issue,
// make your content struct (or one which embeds Item) implement Sluggable
// and it will override the slug created by Item's SetSlug with your own
type Sluggable interface {
	SetSlug(string)
	ItemSlug() string
}

// Identifiable enables a struct to have its ID set/get. Typically this is done
// to set an ID to -1 indicating it is new for DB inserts, since by default
// a newly initialized struct would have an ID of 0, the int zero-value, and
// BoltDB's starting key per bucket is 0, thus overwriting the first record.
type Identifiable interface {
	ItemID() int
	SetItemID(int)
	UniqueID() uuid.UUID
	String() string
}

// Sortable ensures data is sortable by time
type Sortable interface {
	Time() int64
	Touch() int64
}

// Hookable provides our user with an easy way to intercept or add functionality
// to the different lifecycles/events a struct may encounter. Item implements
// Hookable with no-ops so our user can override only whichever ones necessary.
type Hookable interface {
	BeforeAPIResponse(http.ResponseWriter, *http.Request, []byte) ([]byte, error)
	AfterAPIResponse(http.ResponseWriter, *http.Request, []byte) error

	BeforeAPICreate(http.ResponseWriter, *http.Request) error
	AfterAPICreate(http.ResponseWriter, *http.Request) error

	BeforeAPIUpdate(http.ResponseWriter, *http.Request) error
	AfterAPIUpdate(http.ResponseWriter, *http.Request) error

	BeforeAPIDelete(http.ResponseWriter, *http.Request) error
	AfterAPIDelete(http.ResponseWriter, *http.Request) error

	BeforeAdminCreate(http.ResponseWriter, *http.Request) error
	AfterAdminCreate(http.ResponseWriter, *http.Request) error

	BeforeAdminUpdate(http.ResponseWriter, *http.Request) error
	AfterAdminUpdate(http.ResponseWriter, *http.Request) error

	BeforeAdminDelete(http.ResponseWriter, *http.Request) error
	AfterAdminDelete(http.ResponseWriter, *http.Request) error

	BeforeSave(http.ResponseWriter, *http.Request) error
	AfterSave(http.ResponseWriter, *http.Request) error

	BeforeDelete(http.ResponseWriter, *http.Request) error
	AfterDelete(http.ResponseWriter, *http.Request) error

	BeforeApprove(http.ResponseWriter, *http.Request) error
	AfterApprove(http.ResponseWriter, *http.Request) error

	BeforeReject(http.ResponseWriter, *http.Request) error
	AfterReject(http.ResponseWriter, *http.Request) error

	// Enable/Disable used for addons
	BeforeEnable(http.ResponseWriter, *http.Request) error
	AfterEnable(http.ResponseWriter, *http.Request) error

	BeforeDisable(http.ResponseWriter, *http.Request) error
	AfterDisable(http.ResponseWriter, *http.Request) error
}

// Hideable lets a user keep items hidden
type Hideable interface {
	Hide(http.ResponseWriter, *http.Request) error
}

// Pushable lets a user define which values of certain struct fields are
// 'pushed' down to  a client via HTTP/2 Server Push. All items in the slice
// should be the json tag names of the struct fields to which they correspond.
type Pushable interface {
	// the values contained by fields returned by Push must strictly be URL paths
	Push(http.ResponseWriter, *http.Request) ([]string, error)
}

// Omittable lets a user define certin fields within a content struct to remove
// from an API response. Helpful when you want data in the CMS, but not entirely
// shown or available from the content API. All items in the slice should be the
// json tag names of the struct fields to which they correspond.
type Omittable interface {
	Omit(http.ResponseWriter, *http.Request) ([]string, error)
}

// Item should only be embedded into content type structs.
type Item struct {
	UUID      uuid.UUID `json:"uuid"`
	ID        int       `json:"id"`
	Slug      string    `json:"slug"`
	Timestamp int64     `json:"timestamp"`
	Updated   int64     `json:"updated"`
}

// Time partially implements the Sortable interface
func (i Item) Time() int64 {
	return i.Timestamp
}

// Touch partially implements the Sortable interface
func (i Item) Touch() int64 {
	return i.Updated
}

// SetSlug sets the item's slug for its URL
func (i *Item) SetSlug(slug string) {
	i.Slug = slug
}

// ItemSlug sets the item's slug for its URL
func (i *Item) ItemSlug() string {
	return i.Slug
}

// ItemID gets the Item's ID field
// partially implements the Identifiable interface
func (i Item) ItemID() int {
	return i.ID
}

// SetItemID sets the Item's ID field
// partially implements the Identifiable interface
func (i *Item) SetItemID(id int) {
	i.ID = id
}

// UniqueID gets the Item's UUID field
// partially implements the Identifiable interface
func (i Item) UniqueID() uuid.UUID {
	return i.UUID
}

// String formats an Item into a printable value
// partially implements the Identifiable interface
func (i Item) String() string {
	return fmt.Sprintf("Item ID: %s", i.UniqueID())
}

// BeforeAPIResponse is a no-op to ensure structs which embed Item implement Hookable
func (i Item) BeforeAPIResponse(res http.ResponseWriter, req *http.Request, data []byte) ([]byte, error) {
	return data, nil
}

// AfterAPIResponse is a no-op to ensure structs which embed Item implement Hookable
func (i Item) AfterAPIResponse(res http.ResponseWriter, req *http.Request, data []byte) error {
	return nil
}

// BeforeAPICreate is a no-op to ensure structs which embed Item implement Hookable
func (i Item) BeforeAPICreate(res http.ResponseWriter, req *http.Request) error {
	return nil
}

// AfterAPICreate is a no-op to ensure structs which embed Item implement Hookable
func (i Item) AfterAPICreate(res http.ResponseWriter, req *http.Request) error {
	return nil
}

// BeforeAPIUpdate is a no-op to ensure structs which embed Item implement Hookable
func (i Item) BeforeAPIUpdate(res http.ResponseWriter, req *http.Request) error {
	return nil
}

// AfterAPIUpdate is a no-op to ensure structs which embed Item implement Hookable
func (i Item) AfterAPIUpdate(res http.ResponseWriter, req *http.Request) error {
	return nil
}

// BeforeAPIDelete is a no-op to ensure structs which embed Item implement Hookable
func (i Item) BeforeAPIDelete(res http.ResponseWriter, req *http.Request) error {
	return nil
}

// AfterAPIDelete is a no-op to ensure structs which embed Item implement Hookable
func (i Item) AfterAPIDelete(res http.ResponseWriter, req *http.Request) error {
	return nil
}

// BeforeAdminCreate is a no-op to ensure structs which embed Item implement Hookable
func (i Item) BeforeAdminCreate(res http.ResponseWriter, req *http.Request) error {
	return nil
}

// AfterAdminCreate is a no-op to ensure structs which embed Item implement Hookable
func (i Item) AfterAdminCreate(res http.ResponseWriter, req *http.Request) error {
	return nil
}

// BeforeAdminUpdate is a no-op to ensure structs which embed Item implement Hookable
func (i Item) BeforeAdminUpdate(res http.ResponseWriter, req *http.Request) error {
	return nil
}

// AfterAdminUpdate is a no-op to ensure structs which embed Item implement Hookable
func (i Item) AfterAdminUpdate(res http.ResponseWriter, req *http.Request) error {
	return nil
}

// BeforeAdminDelete is a no-op to ensure structs which embed Item implement Hookable
func (i Item) BeforeAdminDelete(res http.ResponseWriter, req *http.Request) error {
	return nil
}

// AfterAdminDelete is a no-op to ensure structs which embed Item implement Hookable
func (i Item) AfterAdminDelete(res http.ResponseWriter, req *http.Request) error {
	return nil
}

// BeforeSave is a no-op to ensure structs which embed Item implement Hookable
func (i Item) BeforeSave(res http.ResponseWriter, req *http.Request) error {
	return nil
}

// AfterSave is a no-op to ensure structs which embed Item implement Hookable
func (i Item) AfterSave(res http.ResponseWriter, req *http.Request) error {
	return nil
}

// BeforeDelete is a no-op to ensure structs which embed Item implement Hookable
func (i Item) BeforeDelete(res http.ResponseWriter, req *http.Request) error {
	return nil
}

// AfterDelete is a no-op to ensure structs which embed Item implement Hookable
func (i Item) AfterDelete(res http.ResponseWriter, req *http.Request) error {
	return nil
}

// BeforeApprove is a no-op to ensure structs which embed Item implement Hookable
func (i Item) BeforeApprove(res http.ResponseWriter, req *http.Request) error {
	return nil
}

// AfterApprove is a no-op to ensure structs which embed Item implement Hookable
func (i Item) AfterApprove(res http.ResponseWriter, req *http.Request) error {
	return nil
}

// BeforeReject is a no-op to ensure structs which embed Item implement Hookable
func (i Item) BeforeReject(res http.ResponseWriter, req *http.Request) error {
	return nil
}

// AfterReject is a no-op to ensure structs which embed Item implement Hookable
func (i Item) AfterReject(res http.ResponseWriter, req *http.Request) error {
	return nil
}

// BeforeEnable is a no-op to ensure structs which embed Item implement Hookable
func (i Item) BeforeEnable(res http.ResponseWriter, req *http.Request) error {
	return nil
}

// AfterEnable is a no-op to ensure structs which embed Item implement Hookable
func (i Item) AfterEnable(res http.ResponseWriter, req *http.Request) error {
	return nil
}

// BeforeDisable is a no-op to ensure structs which embed Item implement Hookable
func (i Item) BeforeDisable(res http.ResponseWriter, req *http.Request) error {
	return nil
}

// AfterDisable is a no-op to ensure structs which embed Item implement Hookable
func (i Item) AfterDisable(res http.ResponseWriter, req *http.Request) error {
	return nil
}

// SearchMapping returns a default implementation of a Bleve IndexMappingImpl
// partially implements search.Searchable
func (i Item) SearchMapping() (*mapping.IndexMappingImpl, error) {
	mapping := bleve.NewIndexMapping()
	mapping.StoreDynamic = false

	return mapping, nil
}

// IndexContent determines if a type should be indexed for searching
// partially implements search.Searchable
func (i Item) IndexContent() bool {
	return false
}

// Slug returns a URL friendly string from the title of a post item
func Slug(i Identifiable) (string, error) {
	// get the name of the post item
	name := strings.TrimSpace(i.String())

	// filter out non-alphanumeric character or non-whitespace
	slug, err := stringToSlug(name)
	if err != nil {
		return "", err
	}

	return slug, nil
}

func isMn(r rune) bool {
	return unicode.Is(unicode.Mn, r) // Mn: nonspacing marks
}

// modified version of: https://www.socketloop.com/tutorials/golang-format-strings-to-seo-friendly-url-example
func stringToSlug(s string) (string, error) {
	src := []byte(strings.ToLower(s))

	// Range over compiled regex and replacements from init().
	for rx := range rxList {
		src = rx.ReplaceAll(src, rxList[rx])
	}

	str := strings.Replace(string(src), "'", "", -1)
	str = strings.Replace(str, `"`, "", -1)
	str = strings.Replace(str, "&", "-", -1)

	t := transform.Chain(norm.NFD, transform.RemoveFunc(isMn), norm.NFC)
	slug, _, err := transform.String(t, str)
	if err != nil {
		return "", err
	}

	return strings.TrimSpace(slug), nil
}

// NormalizeString removes and replaces illegal characters for URLs and other
// path entities. Useful for taking user input and converting it for keys or URLs.
func NormalizeString(s string) (string, error) {
	return stringToSlug(s)
}
